package tester.utilities;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.TreeSet;

import org.apache.log4j.Logger;

import tester.Clone;
import tester.EffectField;

/**
 * Deep clones an object using reflection. This is a <i>relatively</i> slow
 * process compared with serialization, shallow clones, etc., but offers an
 * extremely accurate clone that can be used to measure subtle side-effects in
 * imperative testing.
 * 
 * @author Weston Jossey
 * @since December 12 2008
 * @version 1.0
 * @param <T>
 *            The type to clone.
 */
public class TesterUtility<T> {
	private static final Logger logger = Logger.getLogger(TesterUtility.class);
	private HashMap<Integer, Clone<Object>> hash;
	private final T valueToClone;
	private final EffectField[] effectFields;
	private ArrayList<Clone<?>> oldObjects = new ArrayList<Clone<?>>();

	/**
	 * Constructs a Clone object with the given object to clone, but does not
	 * actually do the cloning (this is done in getClone() so as to allow for
	 * multi-threading if it is so desired).
	 * 
	 * @param valueToClone
	 *            The value to clone
	 * @param effectFields
	 *            The fields that you would like to modify during the clone.
	 */
	public TesterUtility(T valueToClone, EffectField... effectFields)
			throws NullPointerException {
		hash = new HashMap<Integer, Clone<Object>>();
		this.valueToClone = valueToClone;
		this.effectFields = effectFields;
		init();
	}

	/**
	 * Constructs a Clone object with the given object to clone, but does not
	 * actually do the cloning (this is done in getClone() so as to allow for
	 * multi-threading, if it is so desired).
	 * 
	 * @param valueToClone
	 *            The value to clone
	 */
	public TesterUtility(T valueToClone) throws NullPointerException {
		this(valueToClone, (EffectField) null);
	}

	/*
	 * Take the effectFields and parse them, and add them to the
	 * ArrayList oldObjects to be used elsewhere in the code.  
	 */
	private void init() {
		if(effectFields != null){
			Reflector r1 = new Reflector(valueToClone);
			for (EffectField e : effectFields) {
				if(e != null){
					Object oldObj = null;
					try {

						Field oldField = r1.findField(e.getName());
						oldField.setAccessible(true);
						oldObj = oldField.get(valueToClone);
						if (oldObj != null) {
							oldObjects.add(new Clone<Object>(e.getValue(), oldObj));
						}
					} catch (SecurityException e1) {
						e1.printStackTrace();
					} catch (IllegalArgumentException e1) {
						e1.printStackTrace();
					} catch (IllegalAccessException e1) {
						e1.printStackTrace();
					}
				}
			}
		}
	}

	/**
	 * Private internal constructor that also takes a hash as a parameter so
	 * that we can store references to objects that may be in the process of
	 * being cloned, but not necessarily cloned yet. The reason we do this is so
	 * that if we run into the issue of circularly referential data, then we
	 * will not continue deep cloning a circular object, but rather refer to the
	 * reference to the cloned instance.
	 * 
	 * @param valueToClone
	 *            The value to clone
	 * @param hash
	 *            The hash containing any references to other cloned objects.
	 */
	private TesterUtility(T valueToClone, HashMap<Integer, Clone<Object>> hash) {
		this.valueToClone = valueToClone;
		this.hash = hash;
		this.effectFields = null;
	}

	/**
	 * This clones the object which was passed to the constructor, and returns
	 * the clone as the return value.
	 * 
	 * 
	 * 
	 * @return The cloned instance of the object passed to the constructor.
	 */
	/* This is the first method in the execution series */
	public T getClone() {
		if (this.valueToClone instanceof TreeSet) {
			/*
			 * I've run into an issue where I am unable to clone TreeSet
			 * properly because of some issues regarding synthetic inner
			 * classes, and the java reflection package not handling them
			 * properly. I do not believe that this is an issue with my cloner,
			 * as I seem to be able to get this to work for all cases so far
			 * EXCEPT TreeSet.
			 */
			try {
				return SerializeClone.<T>deepCopy(this.valueToClone);
			} catch (Exception e) {
				// The catch falls to the otherwise...
				logger.warn("Class: "
						+ valueToClone.getClass().getCanonicalName()
						+ " should implement Serializable.");
			}
		}
		// Otherwise...

		/*
		 * Now, I went through some debate deciding whether to use a clone
		 * method provided to us by the user, and I decided to pass. Many clone
		 * methods will be shallow clones, leaving most references intact. Since
		 * our void tester requires that we not have references (since we need
		 * to detect subtle differences that are the side-effects we are trying
		 * to detect), shallow clones are no good.
		 */
		if (isPrimitive(valueToClone)) {
			return getPrimitiveClone();

		} else {
			T returnVal = getNonPrimitive();
			if (effectFields != null && returnVal != null) {
				findAndReplacePrimitives(returnVal);
			}
			return returnVal;
		}

	}

	/*
	 * Either we have objects or arrays...
	 * 
	 * Step two of the execution series (the other possible step 2 is
	 * getPrimitiveClone )
	 */
	private T getNonPrimitive() {
		if (this.valueToClone.getClass().isArray()) {
			return getArrayClone();
		} else {
			return getObjectClone();
		}
	}

	@SuppressWarnings("unchecked")
	/*
	 * For arrays, we handle this specially
	 * 
	 * Step three of the execution series (the other possible step 3 is
	 * getObjectClone )
	 */
	private T getArrayClone() {

		/* The array case is somewhat tricky... */

		Class<?> type = this.valueToClone.getClass().getComponentType();
		Object returnArray;

		// Put this return value into the hash.
		if (!hash.containsKey(getHash(this.valueToClone))) {
			returnArray = Array.newInstance(type, Array
					.getLength(this.valueToClone));
			hash.put(getHash(this.valueToClone), new Clone<Object>(returnArray,
					this.valueToClone));
		} else {
			/* The array already exists in the hash, so retrieve the clone */
			returnArray = hash.get(getHash(this.valueToClone)).getClone();
		}

		/*
		 * For each item in the array, check to see if there is an instance of
		 * the object in the hash, and handle appropriately.
		 */
		for (int i = 0; i < Array.getLength(this.valueToClone); i++) {

			/* Object to clone */
			Object arrayObject = Array.get(this.valueToClone, i);

			/* Clone of the object */
			Object cloneArrayObject;
			if (arrayObject != null) {

				/* Check the hash for the original array object */
				if (hash.containsKey(getHash(arrayObject))
						&& hash.get(getHash(arrayObject)).getOriginal() == arrayObject) {
					// Hit!
					cloneArrayObject = hash.get(getHash(arrayObject))
							.getClone();

				} else if (!isPrimitive(arrayObject)
						|| arrayObject.getClass().isArray()) {

					/* We have a non-primitive... Generate a new instance */
					if (arrayObject.getClass().isArray()) {
						cloneArrayObject = newArrayInstance(arrayObject);
					} else {
						cloneArrayObject = newObjectInstance(arrayObject);
					}

					/* Put it in the hash */
					hash.put(getHash(arrayObject), new Clone<Object>(
							cloneArrayObject, arrayObject));

					/* Clone */
					cloneArrayObject = new TesterUtility<Object>(arrayObject,
							hash).getClone();
				} else {
					/*
					 * Otherwise it's a primitive, so we don't care about
					 * references
					 */
					cloneArrayObject = new TesterUtility<Object>(arrayObject,
							hash).getClone();
				}
			} else {
				/* The original object was null, so the clone is also */
				cloneArrayObject = null;
			}
			/*
			 * Set the value of the returnArray at location i to the value of
			 * the returnVal
			 */
			Array.set(returnArray, i, cloneArrayObject);
		}

		/* Return our cloned array */
		return (T) returnArray;

	}

	/**
	 * Retrieves the clone with the effected fields (if applicable)
	 * 
	 * @return The cloned object with modifications (if applicable)
	 */
	@SuppressWarnings("unchecked")
	private T getObjectClone() {
		/*
		 * The general case for cloning. If this fails, we were unable to clone
		 * the object
		 */

		T instanceClone = null;
		Reflector reflector = new Reflector(this.valueToClone);

		/* These are the fields from the object that we need to clone. */
		Field[] fieldsToClone = reflector.getFields(this.valueToClone);

		try {

			/* Get a new instance of the valueToClone */
			if (!hash.containsKey(getHash(this.valueToClone))) {

				/* If the hash does not contain a reference to the valueToClone */
				instanceClone = (T) newObjectInstance(this.valueToClone);
				hash.put(getHash(this.valueToClone), new Clone<Object>(
						instanceClone, this.valueToClone));
			} else {
				instanceClone = findAndReplace(this.valueToClone);
				if (instanceClone != null) {
					hash.put(getHash(this.valueToClone), new Clone<Object>(
							instanceClone, this.valueToClone));
				} else {
					// Last resort
					instanceClone = (T) hash.get(getHash(this.valueToClone))
							.getClone();
				}
			}

			for (Field f : fieldsToClone) {
				if (Modifier.isStatic(f.getModifiers()))
					continue;

				/* For each field in the object we are cloning */

				/* Initialize some values */
				Object fieldValueToClone = f.get(this.valueToClone);
				Object fieldReturnVal = null;

				fieldReturnVal = findAndReplace(fieldValueToClone);
				if (fieldReturnVal != null) {
					hash.put(getHash(fieldValueToClone), new Clone<Object>(
							fieldReturnVal, fieldValueToClone));
				} else if (fieldValueToClone != null) {
					if (Modifier.isFinal(f.getModifiers())) {
						fieldReturnVal = fieldValueToClone;
					} else if (hash.containsKey(getHash(fieldValueToClone))
							&& hash.get(getHash(fieldValueToClone))
									.getOriginal() == fieldValueToClone) {
						// If the hash has a reference...
						fieldReturnVal = hash.get(getHash(fieldValueToClone))
								.getClone();
					} else if (!isPrimitive(fieldValueToClone)
							|| fieldValueToClone.getClass().isArray()) {

						if (fieldValueToClone.getClass().isArray()) {
							fieldReturnVal = newArrayInstance(fieldValueToClone);
						} else {
							// If the field is not primitive or it's an array...
							fieldReturnVal = newObjectInstance(fieldValueToClone);
						}

						hash.put(getHash(fieldValueToClone), new Clone<Object>(
								fieldReturnVal, fieldValueToClone));
						fieldReturnVal = new TesterUtility<Object>(
								fieldValueToClone, hash).getClone();

					} else {
						// Otherwise...
						fieldReturnVal = new TesterUtility<Object>(
								fieldValueToClone, hash).getClone();
					}
				}

				/*
				 * Attempt: Set the value of the field of the return object to
				 * the cloned value of the field of the original object.
				 */
				try {
					ReflectionHelper.setStaticFinalField(instanceClone, f,
							fieldReturnVal);
				} catch (IllegalArgumentException e) {
					logger.error(e.getLocalizedMessage());
					e.printStackTrace();
					logger.error(instanceClone.toString());
					logger.error(f.toString());
					logger.error(fieldReturnVal.toString());
				}
			}

		} catch (IllegalAccessException e) {
			logger.error(e.getLocalizedMessage());
			e.printStackTrace();
		} catch (NoSuchFieldException e) {
			logger.error(e.getLocalizedMessage());
			e.printStackTrace();
		}

		/* Return the cloned value of valueToClone */
		return instanceClone;
	}

	@SuppressWarnings("unchecked")
	private T getPrimitiveClone() {
		/*
		 * Note that for primitive types: Java will automatically convert
		 * primitive types (int, long, boolean, ... ) into their wrapper class
		 * (Integer, Long, Boolean, ... )
		 */
		
		if (this.valueToClone.getClass() == java.lang.Integer.class
				|| this.valueToClone.getClass() == java.lang.Integer.TYPE) {
			/* Case for an int or Integer */
			return (T) new Integer(((Integer) this.valueToClone).intValue());

		} else if (this.valueToClone.getClass() == java.lang.String.class) {
			/* String case */
			return (T) new String((String) this.valueToClone);

		} else if (this.valueToClone.getClass() == BigInteger.class) {
			/* BigInteger case */
			return (T) new BigInteger(((BigInteger) this.valueToClone)
					.toByteArray());

		} else if (this.valueToClone.getClass() == BigDecimal.class) {
			/* Case for BigDecimal */
			return (T) new BigDecimal(((BigDecimal) this.valueToClone)
					.toEngineeringString());

		} else if (this.valueToClone.getClass() == java.lang.Boolean.class
				|| this.valueToClone.getClass() == java.lang.Boolean.TYPE) {
			/* Case for a boolean or Boolean */
			return (T) new Boolean(((Boolean) this.valueToClone).booleanValue());

		} else if (this.valueToClone.getClass() == java.lang.Byte.class
				|| this.valueToClone.getClass() == java.lang.Byte.TYPE) {
			/* Case for a byte or Byte */
			return (T) new Byte(((Byte) this.valueToClone).byteValue());

		} else if (this.valueToClone.getClass() == java.lang.Character.class
				|| this.valueToClone.getClass() == java.lang.Character.TYPE) {
			/* Case for char or Character */
			return (T) new Character(((Character) this.valueToClone)
					.charValue());

		} else if (this.valueToClone.getClass() == java.lang.Double.class
				|| this.valueToClone.getClass() == java.lang.Double.TYPE) {
			/* Case for a double or Double */
			return (T) new Double(((Double) this.valueToClone).doubleValue());

		} else if (this.valueToClone.getClass() == java.lang.Float.class
				|| this.valueToClone.getClass() == java.lang.Float.TYPE) {
			/* Case for float or Float */
			return (T) new Float(((Float) this.valueToClone).floatValue());

		} else if (this.valueToClone.getClass() == java.lang.Long.class
				|| this.valueToClone.getClass() == java.lang.Long.TYPE) {
			/* Case for long or Long */
			return (T) new Long(((Long) this.valueToClone).longValue());

		} else if (this.valueToClone.getClass() == java.lang.Short.class
				|| this.valueToClone.getClass() == java.lang.Short.TYPE) {
			/* Case for short or Short */
			return (T) new Short(((Short) this.valueToClone).shortValue());

		} else {
			return null;
		}
	}

	/**
	 * Returns a new instance of the valueToClone. Specific for objects that may
	 * or may not have a default constructor.
	 * 
	 * @return
	 */
	@SuppressWarnings("unchecked")
	private Object newObjectInstance(Object objectToClone) {
		try {
			/* Try to use the default constructor */
			return objectToClone.getClass().newInstance();
		} catch (InstantiationException e) {
			logger.warn("\nNo default constructor: " + objectToClone.toString()
					+ "\nSetting the return value to be a "
					+ "reference of the value to clone.  "
					+ "The clone attempt has failed.");

		} catch (IllegalAccessException e) {
			logger.warn("Illegal Access Exception");
		}

		Constructor<?>[] c = objectToClone.getClass().getDeclaredConstructors();

		for (Constructor<?> constructor : c) {
			/* For each constructor of the value to clone */
			constructor.setAccessible(true);
			Object[] params = new Object[constructor.getParameterTypes().length];

			for (int i = 0; i < params.length; i++) {
				if (constructor.getParameterTypes()[i].isPrimitive()) {
					// If it's primitive, get a dummy value
					params[i] = getPrimitive(constructor.getParameterTypes()[i]);
				} else {
					// Else we can just set it to null
					params[i] = null;
				}
			}

			try {
				if (logger.isDebugEnabled()) {
					logger
							.debug("Attempting to construct with constructor...\n"
									+ constructor.getName());
					logger.debug(constructor.isAccessible());
				}

				if (constructor.getClass().isSynthetic()) {
					// Ignore synthetic constructors
					continue;
				}

				if (Modifier.isPrivate(constructor.getModifiers()))
					continue;
				T returnVal = (T) constructor.newInstance(params);

				if (returnVal != null)
					return returnVal;
				else
					continue;

			} catch (IllegalArgumentException e1) {
				e1.printStackTrace();
			} catch (InstantiationException e1) {
				e1.printStackTrace();
			} catch (IllegalAccessException e1) {
				e1.printStackTrace();
			} catch (InvocationTargetException e1) {
				e1.printStackTrace();
			} catch (Exception e1) {
				e1.printStackTrace();
			}
		}

		logger.warn("Unable to instantiate the method.  We are returning "
				+ "a reference to the original, non-cloneable, object for"
				+ objectToClone.toString());
		return objectToClone;
	}

	private Object newArrayInstance(Object objectToClone) {

		/* Get the component type, and instantiate a new instance */
		Class<?> type = objectToClone.getClass().getComponentType();
		return Array.newInstance(type, Array.getLength(objectToClone));
	}

	/**
	 * Internal method that returns true if the given object has a clone method
	 * to call.
	 * 
	 * @param t
	 *            The value to test for a clone method
	 * @return Returns true if the given object has a clone method.
	 */
	@SuppressWarnings("unused")
	private boolean hasCloneMethod(T t) {
		try {
			t.getClass().getDeclaredMethod("clone", (Class[]) null);
			return true;
		} catch (SecurityException e) {
			logger.error(e.getLocalizedMessage());
			return false;
		} catch (NoSuchMethodException e) {
			logger.debug("No clone method present for: "
					+ t.getClass().getName());
			return false;
		}
	}

	private int getHash(Object o) {
		return o.hashCode();
	}

	/**
	 * Returns true if the object is a primitive class or TYPE.
	 * 
	 * @param o
	 *            object to test to see if it is primitive
	 * @return true if it is primitive, otherwise false
	 */
	private boolean isPrimitive(Object o) {
		return o.getClass() == java.lang.Double.class
				|| o.getClass() == java.lang.Double.TYPE
				|| o.getClass() == java.lang.Integer.class
				|| o.getClass() == java.lang.Integer.TYPE
				|| o.getClass() == java.lang.Byte.class
				|| o.getClass() == java.lang.Byte.TYPE
				|| o.getClass() == java.lang.Float.class
				|| o.getClass() == java.lang.Float.TYPE
				|| o.getClass() == java.lang.String.class
				|| o.getClass() == java.lang.Boolean.class
				|| o.getClass() == java.lang.Boolean.TYPE
				|| o.getClass() == java.lang.Character.class
				|| o.getClass() == java.lang.Character.TYPE
				|| o.getClass() == java.lang.Long.class
				|| o.getClass() == java.lang.Long.TYPE
				|| o.getClass() == java.lang.Short.class
				|| o.getClass() == java.lang.Short.TYPE;
	}

	/**
	 * Returns a "dummy" primitive value to be used in instantiating non-default
	 * constructors
	 * 
	 * @param c
	 *            Class to create a new instance of
	 * @return dummy object
	 */
	private Object getPrimitive(Class<?> c) {
		if (c == java.lang.Double.TYPE || c == java.lang.Double.class) {
			return new Double(0);
		} else if (c == java.lang.Integer.TYPE || c == java.lang.Integer.class) {
			return new Integer(0);
		} else if (c == java.lang.Byte.TYPE || c == java.lang.Byte.class) {
			return new Byte("a");
		} else if (c == java.lang.Boolean.TYPE || c == java.lang.Boolean.class) {
			return new Boolean(false);
		} else if (c == java.lang.Character.TYPE
				|| c == java.lang.Character.class) {
			return new Character('a');
		} else if (c == java.lang.Long.TYPE || c == java.lang.Long.class) {
			return new Long(0);
		} else if (c == java.lang.Short.TYPE || c == java.lang.Short.class) {
			return new Short("a");
		} else {
			return new Object();
		}
	}

	/**
	 * Find and replace is needed when we are doing mutations to the object that
	 * we are cloning. We use this methodology because we want all instances to
	 * point to a common reference.
	 * 
	 * @param value
	 *            original value we are attempting to clone
	 * @return the reference to the clone (returns null if this object is not
	 *         expected to be mutated)
	 */
	@SuppressWarnings("unchecked")
	public T findAndReplace(Object value) {
		for (Clone<?> clone : oldObjects) {
			if (clone.getOriginal() == value) {
				return (T) clone.getClone();
			}
		}
		return null;
	}

	/**
	 * This is used to find and replace primitives that we expect to change due
	 * to side-effects. We are currently restricted to only being able to change
	 * the values of the base-object. In other words, we can not mutate the
	 * fields of a field of the base-object.
	 * 
	 * @param returnVal
	 *            the modified clone
	 */
	public void findAndReplacePrimitives(T returnVal) {
		Reflector r1 = new Reflector(returnVal);
		for (EffectField e : effectFields) {
			if(e != null){
				if (isPrimitive(e.getValue())) {
					try {
						Field fieldOfVal = r1.findField(e.getName());
						if (fieldOfVal != null) {
							fieldOfVal.set(returnVal, e.getValue());
						}
					} catch (IllegalArgumentException e1) {
						// TODO Auto-generated catch block
						e1.printStackTrace();
					} catch (IllegalAccessException e1) {
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
				}
			}
		}
	}

	/**
	 * Static way of creating a clone
	 * 
	 * @param <T>
	 *            type of the object to copy
	 * @param source
	 *            the object to copied
	 * @return the copied object
	 */
	public static <T> T objectCopy(T source) {
		return new TesterUtility<T>(source).getClone();
	}

}

class SerializeClone {
	private static final Logger logger = Logger.getLogger(SerializeClone.class);

	// so that nobody can accidentally create an ObjectCloner object
	private SerializeClone() {
	}

	// returns a deep copy of an object
	@SuppressWarnings("unchecked")
	static public <T> T deepCopy(T valueToClone) throws Exception {
		ObjectOutputStream objectOutputStream = null;
		ObjectInputStream objectInputStream = null;
		try {
			ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
			objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
			// serialize
			objectOutputStream.writeObject(valueToClone);
			objectOutputStream.flush();
			ByteArrayInputStream bin = new ByteArrayInputStream(
					byteArrayOutputStream.toByteArray());
			objectInputStream = new ObjectInputStream(bin);
			// return the clone
			return (T) objectInputStream.readObject();
		} catch (Exception e) {
			logger.warn("Exception in deepCopy = " + e);
			throw (e);
		} finally {
			objectOutputStream.close();
			objectInputStream.close();
		}
	}

}
